
'use strict';

/**
	Part of Zombie Navigator
	Copyright © 2015  Zombie Navigator Developers
	
	@file
	@module
	@license AGPL-3.0+
*/


/* Addon SDK dependencies */
const _sdkWindowAPI = require('sdk/windows').browserWindows;
const _sdkIsPrivate = require('sdk/private-browsing').isPrivate;


/* Used modules */
const _modBrowserTab = require('./internal/browser-tab');
const _modActivityTracker = require('./internal/activity-tracker');
const _modMeta = require('./meta');


/* Private properties */

const _closeCallbacks = new Set;

const _zombiePageURI = _modMeta.getURIFromDataPath('internal-pages/zombie-window/zombie-window.html');

/**
	@private
*/
const _maybeCallback = callback => (
	('function' == typeof callback) ? callback : (() => void(0)) // or Function()
);

/**
	@private
*/
const _sdkOpenWindow = ({onOpen, onClose, isPrivate, controllerURI}) => {
	onOpen = _maybeCallback(onOpen);
	onClose = _maybeCallback(onClose);
	controllerURI = '' + controllerURI; // shown on the activities window
	
	return _sdkWindowAPI.open({
		url: _zombiePageURI
		, onOpen: sdkWindow => {
			sdkWindow.tabs.once('ready', sdkTab => {
				/*
					Monitor activities on this window
					
					This script is run on the trusted resource:// page
					(_zombiePageURI)
					
					If the user moves a tab into another window, *this*
					monitoring code loses the effect on that tab. Workarounds
					exists for that but they are not worth the code complexity
					in this addon, which are mainly noninteractive by nature.
				*/
				
				const modMeta = _modMeta;
				
				
				// a static small contentScript
				let worker = sdkTab.attach({
					contentScript: `
						self.port.on("data", data => {
							try {
								window.postMessage(data, "*")
							} catch (e) {}
						})
					`
				});
				
				let workerIsDetached = false;
				worker.on('detach', () => {
					workerIsDetached = true;
				});
				
				
				/* Monitor events on this window */
				
				sdkWindow.tabs.on('open', sdkTab => {
					if (workerIsDetached) return;
					
					worker.port.emit('data', {
						type: 'open'
						, isPrivate: _sdkIsPrivate(sdkTab)
					});
				});
				
				sdkWindow.tabs.on('ready', sdkTab => {
					if (workerIsDetached) return;
					
					worker.port.emit('data', {
						type: 'ready'
						, isPrivate: _sdkIsPrivate(sdkTab)
						, uri: sdkTab.url
					});
				});
				
				sdkWindow.tabs.on('close', sdkTab => {
					if (workerIsDetached) return;
					
					worker.port.emit('data', {
						type: 'close'
						, isPrivate: _sdkIsPrivate(sdkTab)
						, uri: sdkTab.url
					});
				});
				
				
				/* Send metadata */
				
				worker.port.emit('data', {
					type: 'systemInfo'
					, withoutUri: true
					, system: '' + modMeta.getSystemDescription()
					, platform: '' + modMeta.getPlatformName()
					, platformVersion: '' + modMeta.getPlatformVersion()
					, addon: '' + modMeta.getProductName()
					, addonVersion: '' + modMeta.getProductVersion()
				});
				
				worker.port.emit('data', {
					type: 'controllerInfo'
					, uri: controllerURI
				});
				
				
				/*
					Return once the monitoring script is initialized
				*/
				onOpen(sdkWindow);
			});
		}
		, onClose: onClose
		, isPrivate: !!isPrivate
	});
};


/* Exported APIs */

/*
	Resource classes
*/

/**
	Non-private resources
	@public
	@type {symbol}
*/
const CLASS_NON_PRIVATE = Symbol();

/**
	Private resources
	@public
	@type {symbol}
*/
const CLASS_PRIVATE = Symbol();


/**
	Create a Web Browser Control Group context
	A context can have zero or more windows.
	@param {string} controllerURI the URI that represents the controling owner
	@public
*/
const createControlGroup = controllerURI => {
	/**
		All tabs managed in this control group
		@private
	*/
	const _sdkTabs = new Set;
	
	/**
		SDK window
		If Firefox is configured to force Private Browsing, this points to the same
		window as `_sdkPrivateWindow`.
		@private
	*/
	let _sdkWindow;
	let _sdkWindowTracker = _modActivityTracker.openWindowTracker();
	
	/**
		SDK private window
		@private
	*/
	let _sdkPrivateWindow;
	let _sdkPrivateWindowTracker = _modActivityTracker.openWindowTracker();
	
	
	const _sdkWindowCallbacks = [];
	const _sdkPrivateWindowCallbacks = [];
	
	const _sdkWindowCloseHandler = isPrivate => {
		return () => {
			/*
				Close remaining tabs even if they are moved to different windows
			*/
			if (isPrivate) {
				context.closeAll(CLASS_PRIVATE);
				_sdkPrivateWindow = undefined;
			} else {
				context.closeAll(CLASS_NON_PRIVATE);
				_sdkWindow = undefined;
			}
		};
	};
	
	const _sdkGetWindow = callback => {
		const modActivityTracker = _modActivityTracker;
		
		callback = _maybeCallback(callback);
		
		if (!_sdkWindow) {
			_sdkWindowCallbacks.push(callback);
			
			if (1 >= _sdkWindowCallbacks.length) {
				let handler = sdkWindow => {
					_sdkWindow = sdkWindow;
					
					for (let callback of _sdkWindowCallbacks) {
						callback(_sdkWindow);
					}
					
					_sdkWindowCallbacks.length = 0;
				};
				
				let sdkWindow = _sdkOpenWindow({
					onOpen: handler
					, controllerURI: controllerURI
					, onClose: _sdkWindowCloseHandler(false)
				});
				
				_sdkWindowTracker.addWindow(sdkWindow);
				if (_sdkIsPrivate(sdkWindow)) {
					// Enforced Private Browsing detected
					sdkWindow.close();
					_sdkGetPrivateWindow(handler);
				}
			}
			
		} else {
			callback(_sdkWindow);
		}
	};
	
	const _sdkGetPrivateWindow = (callback) => {
		callback = _maybeCallback(callback);
		
		if (!_sdkPrivateWindow) {
			_sdkPrivateWindowCallbacks.push(callback);
			
			if (1 >= _sdkPrivateWindowCallbacks.length) {
				let handler = sdkPrivateWindow => {
					_sdkPrivateWindow = sdkPrivateWindow;
					
					for (let callback of _sdkPrivateWindowCallbacks) {
						callback(_sdkPrivateWindow);
					}
					
					_sdkPrivateWindowCallbacks.length = 0;
				};
				
				let sdkPrivateWindow = _sdkOpenWindow({
					onOpen: handler
					, isPrivate: true
					, controllerURI: controllerURI
					, onClose: _sdkWindowCloseHandler(true)
				});
				
				_sdkPrivateWindowTracker.addWindow(sdkPrivateWindow);
			}
			
		} else {
			callback(_sdkPrivateWindow)
		}
	};
	
	const _sdkTabOpenHandler = sdkTab => {
		_sdkTabs.add(sdkTab);
		
		sdkTab.on('close', function () {
			_sdkTabs.delete(sdkTab); // make sure this always happen
			
			if (_sdkTabs.size < 1) {
				context.closeAll();
			}
		});
	};
	
	/**
		@returns {Promise} resolves to an SDK Tab
	*/
	const _sdkOpenTab = (uri, isPrivate) => {
		return new Promise(function (resolve, reject) {
			const sdkGetWindow = isPrivate ? _sdkGetPrivateWindow : _sdkGetWindow;
			try {
				sdkGetWindow(function (sdkWindow) { // callback
					try {
						sdkWindow.tabs.open({
							url: uri || 'about:blank' // we prefer 'URI'
							, inBackground: true
							, onOpen: function onOpen (sdkTab) {
								try {
									sdkTab.once('ready', sdkTab => {
										_sdkTabOpenHandler(sdkTab);
										resolve(sdkTab);
									});
								} catch (e) {
									reject(e + '');
								}
							}
						});
					} catch (e) {
						reject(e + '');
					}
				});
			} catch (e) {
				reject(e + '');
			}
		});
	};
	
	const closeCallback = () => {
		context.closeAll();
	};
	_closeCallbacks.add(closeCallback);
	
	const context = Object.freeze({
		__proto__: null
		,
		/**
			Even if there is a new tab opened after this call, the Promise
			will resolve after all the tabs which were open at the time of the
			call have closed.
		*/
		closeAll (resourceClass) {
			const modActivityTracker = _modActivityTracker;
			let sdkWindow = _sdkWindow, sdkPrivateWindow = _sdkPrivateWindow;
			
			return new Promise((resolve, reject) => {
				(new Promise(function (resolve, reject) {
					if (resourceClass) {
						// Do not close windows when closing tabs exclusively
						resolve(true);
						return;
					}
					
					let closeState = {
						_sdkWindow: true
						, _sdkPrivateWindow: true
					};
					
					let resolveIfReady = function () {
						const sdkWindows = _sdkWindowAPI;
						
						if (closeState._sdkWindow && closeState._sdkPrivateWindow) {
							closeState._sdkWindow = closeState._sdkPrivateWindow = false;
							
							if (modActivityTracker.isSessionClean(_sdkWindowTracker, _sdkPrivateWindowTracker)) {
								// Session clean, safe to close all other windows (asynchronously)
								for (let sdkWindow of sdkWindows) {
									sdkWindow.close();
								}
							}
							
							resolve(true);
						}
					};
					
					if (sdkWindow) {
						closeState._sdkWindow = false;
						sdkWindow.close(function () {
							closeState._sdkWindow = true;
							resolveIfReady();
						});
						
						sdkWindow = undefined;
					}
					
					if (sdkPrivateWindow) {
						closeState._sdkPrivateWindow = false;
						sdkPrivateWindow.close(function () {
							closeState._sdkPrivateWindow = true;
							resolveIfReady();
						});
						
						sdkPrivateWindow = undefined;
					}
					
					resolveIfReady();
				}))
				.then(() => {
					let tabCount = _sdkTabs.size;
					if (1 > tabCount) {
						_closeCallbacks.delete(closeCallback);
						resolve(true);
					}
					
					for (let sdkTab of _sdkTabs) {
						try {
							if (!_sdkIsPrivate(sdkTab)) {
								if (resourceClass == CLASS_PRIVATE) {
									continue;
								}
							} else {
								if (resourceClass == CLASS_NON_PRIVATE) {
									continue;
								}
							}
							
							sdkTab.close(() => {
								tabCount--;
								
								if (1 > tabCount) {
									_closeCallbacks.delete(closeCallback);
									resolve(true);
								}
							});
						} catch (e) {
							continue;
						}
					}
					
				});
			});
		}
		,
		/**
			Open a Web browser window (=tab)
		*/
		openWindow (uri, isPrivate) {
			return new Promise((resolve, reject) => {
				uri = uri ? uri + '' : void(0);
				_sdkOpenTab(uri, !!isPrivate)
				.then(sdkTab => {
					try {
						const modBrowserTab = _modBrowserTab;
						const wrapper = modBrowserTab.createSDKTabWrapper(sdkTab);
						
						resolve(wrapper);
					} catch (e) {
						reject(e + '');
					}
				})
				.catch(e => {
					reject(e + '');
				});
			});
		}
	});
	
	return context;
};

/**
	@public
	@method
*/
const closeAllWindows = () => {
	for (let callback of _closeCallbacks) {
		callback();
	}
};


/* Exports */
exports.createControlGroup = createControlGroup;
exports.CLASS_PRIVATE = CLASS_PRIVATE;
exports.CLASS_NON_PRIVATE = CLASS_NON_PRIVATE;
exports.closeAllWindows = closeAllWindows;


// vim: ts=4 noet ai

